C++ dynamic memory allocation (new and new[] and delete and delete[])

  • 2020-05-19 05:26:03
  • OfStack

C++ dynamic memory allocation (new/new[] and delete/delete[])

To solve this common programming problem, the ability to create and destroy objects at run time is a basic requirement. C has, of course, provides a dynamic memory allocation function malloc () and free (), as well as malloc () variant (realloc: change the size of the allocated memory, calloc: pointer to the memory before initialization), these functions at run time (also called free memory allocated from the heap storage unit, but use the library function need to compute the size of the need to create a memory, prone to error.

So in general the way we exploit memory in C is this:


(void*)malloc(sizeof(void)); 

However, these functions do not work well in C+ +. The constructor does not allow you to initialize an object by passing it a memory address. If we do that, we might

Forget it. Object initialization is not guaranteed in C + +.
Expect something to happen, but it turns out that you accidentally make some kind of change to the object before you initialize it.
Passing an object of the wrong size to it.

Of course, even if we do everything right, people who modify our programs are prone to make the same mistakes. Incorrect initialization is a major cause of programming errors, so it is especially important to ensure that constructor calls are made when creating objects on the heap.

How does C+ + ensure proper initialization and cleanup and allow us to dynamically create objects on the heap?

The answer is to make dynamic object creation the heart of the language. malloc() and free() are library functions and are therefore beyond the control of the compiler. If we have one operator that does dynamic memory allocation and initialization and another that cleans and frees memory, the compiler can guarantee that all object constructors and destructors will be called.

If the original dynamic memory is used, it will be very tedious. The specific code is as follows:


#include<cstdlib> 
#include<cstring> 
#include<iostream> 
using namespace std; 
class Obj 
{ 
 int i,j,k; 
 enum {sz=100}; 
 char buf[sz]; 
public: 
  void initialize() 
  { 
    cout<<"initialize"<<endl; 
    i=k=j=0; 
    memset(buf,0,sz); 
  } 
  void destroy() const 
  { 
   cout<<"destroying Obj"<<endl; 
  } 
}; 
int main() 
{ 
  Obj* obj=(Obj*)malloc(sizeof(Obj)); 
   if(obj!=0) 
  obj->initialize(); 
  obj->destroy(); 
  free(obj); 
 return 0; 
}

In the above line of code, we can see that malloc() is used to allocate memory for objects: obj* Obj = (obj*)malloc(sizeof(obj));
Here the user must determine the length of the object (which is also the cause of the program error # 1). Since it is a block of memory rather than an object, malloc() returns an void*.C++ does not allow an void* to be assigned to any pointer, so it must be mapped. Because malloc() may not find allocated memory (in which case it returns 0), you must check the returned pointer to make sure the memory allocation was successful.

But worst of all: Obj- > initialize (); The user must remember to initialize the object before using it. Note that the constructor is not used because the constructor cannot be called explicitly - it is called by the compiler when the object is created. The problem here is that now users may forget to do initialization when working with objects, so this is a major source of introducing bugs into the program. Many programmers find C's dynamic memory allocation functions confusing. Therefore, C programmers often use the virtual memory mechanism to allocate large arrays of variables in static memory regions to avoid using dynamic memory allocation. Because C++ allows one-like programmers to use library functions safely and effortlessly, C's dynamic memory approach should be avoided. The solution in C++ is to combine all the actions required to create an object in a single operator called new. When an object is created with new(the expression for new), it allocates memory for the object in the heap and calls the constructor for that memory.

Therefore, if we write the following expression foo *fp = new foo(1,2); At run time this is equivalent to calling malloc(sizeof(foo)) and using (1,2) as the parameter table for

foo calls the constructor and returns the value as the result address of the this pointer. Until this pointer is assigned to fp, it's an indeterminate, uninitialized object-we can't even touch it until then. It is automatically assigned the correct foo type, so no mapping is required. The default new also checks to be sure that the memory allocation was successful before passing the address to the constructor, so we don't have to explicitly determine whether the call was successful. Later in this chapter, we will find out what happens if there is no memory available to allocate. We can write an ne w expression for the class to use any available constructor. If the constructor has no arguments, write the new expression without the constructor parameter table:

foo *fp = new foo; We've noticed that the process of creating an object in the heap has become simpler -- just a simple expression with built-in length calculations, type conversions, and security checks. This makes it as easy to create an object in the heap as it is to create an object in the stack.

The opposite of the new expression is the delete expression. The delete expression first calls the destructor and then frees memory (often by calling free()). Just as the new expression returns a pointer to an object, the delete expression requires the address of an object. delete fp; The above expression clears the dynamically assigned object foo created earlier. delete is only used to delete objects created by new. If you create an object with malloc() (or calloc() or realloc()), and then delete it with delete, this behavior is undefined. Because most of the default new and delete implementation mechanisms are

malloc() and free() are used, so it is likely that we will free up memory without calling the destructor. If the pointer to the object being deleted is 0, nothing happens. To do this, it is recommended that you assign the pointer to 0 immediately after the pointer is deleted so that it is not deleted twice. Deleting 1 twice on an object is definitely not a good thing, which can cause problems.

Two things happen when you create an new expression. First, use the operator new to allocate memory, and then call the constructor. In the delete expression, the destructor is called and memory is freed using the operator delete. We can never control the calls to constructors and destructors (otherwise we might accidentally mess with them), but we can change the memory allocation function operators new and delete. The memory allocation system used by new and delete is designed for general purposes. But under special circumstances, it can't meet our needs. The reason for changing the allocation system is to consider efficiency: we may have to create and destroy so many objects for a particular class that this operation becomes a speed bottleneck. C++ allows us to override new and delete to implement our own storage allocation scheme, so we can handle the problem like this.

Another problem is heap fragmentation: allocating different sizes of memory can cause so much fragmentation on the heap that you run out of memory quickly. That is, there may still be memory, but because it is fragmented, we cannot find enough memory to meet our needs. By creating our own memory allocator for a particular class, we can ensure that this does not happen.
In embedded and real-time systems, programs may have to run for long periods of time with limited resources. Such a system might also require allocating memory to take the same amount of time and not allow heap memory to run out or to have a lot of fragmentation. Custom memory allocators are one solution, otherwise programmers would avoid new and delete in this case, thereby losing the valuable benefits of C + +.

When overloading the operators new and delete, it is important to remember to change only the old memory allocation method. The compiler allocates memory with new instead of the default version, and then calls the constructor for that memory. So, while the compiler allocates memory when it encounters new and calls the constructor, when we overload new, only the memory allocation can be changed. (delete has similar limitations.)

When overloading the operator new, you can also replace its behavior when it runs out of memory, so you have to decide what to do in the operator new: return 0, write a loop that calls new-handler, try again to allocate or overload new and delete with an bad_alloc exception handling like overloading any other operator 1. However, you can choose to overload global memory allocation functions, or to use specific allocation functions for specific classes

Overloading global versions of new and delete is an extreme approach when they do not satisfy the entire system. If the global version is overridden, the default versions cannot be accessed at all - even in this overloaded definition.

The overloaded ne w must have an size_t parameter. This parameter, generated by the compiler and passed to us, is the length of the object to allocate memory to. We must return a pointer to an object that is equal to (or greater than) this length, if we have a reason to do so, or a 0 if no storage location is found (in which case, the constructor is not called). However, if the storage location is not found and you cannot just return 0, you should also call new-handler or do exception handling to notify that there is a problem.

The return value of the operator new is an void *, not a pointer to any particular type. What it does is allocate memory, not create an object until the constructor calls it, which is an action guaranteed by the compiler and out of our control.

The operator delete accepts an void * that points to the memory allocated by the operator new. It's an void * because it's a pointer that you get after you call the destructor. The destructor removes the object from the storage location. The return type of the operator delete is void.

Here is a simple example of how to overload global new and delete:


#include <stdlib.h> 
 
void * operator new(size_t sz) 
{ 
  printf("operator new:%d bytes\n",sz); 
  void* m=malloc(sz); 
  if(!m) puts("out of memory"); 
  return 0; 
} 
void operator delete(void* m) 
{ 
puts("operator delete"); 
free(m); 
} 
class s 
{ 
  int i[100]; 
public: 
  s(){puts("s::s()");} 
  ~s(){puts("s::~s()");} 
}; 
int main() 
{ 
 puts("creating & destorying an int "); 
 int* p=new int(47); 
 delete p; 
 puts("creating & destorying an s"); 
 s* S=new s; 
 delete S; 
 puts("creating & destorying an s[3]"); 
 s* SA=new s[3]; 
 delete [] SA; 
}

Here you can see the 1-like form of overloading new and delete. To implement the memory allocator, the standard C library functions malloc() and free() are used (perhaps the default new and delete also use these functions). They also print out information about what they're doing. Note that printf() and puts() are used here instead of i o s t r e a m s. When an i o s t r e a m object is created (like global c i n, c o t e r r), they call new to allocate memory. Using printf() does not enter a deadlock state because it does not call new to initialize itself.

In main(), an object of the internal data type is created to prove that the overloaded new and delete are also called in this case. Then create a single object of type s, and then create an array. With arrays, we can see that additional memory is required to hold information about the number of array objects. In all cases, the global overloaded versions of new and delete are used.

When overloading new and delete for a class, we are still creating an static member function, without explicitly saying static. It also has the same syntax as overloading any other operator 1. When the compiler sees a class object created using new, it selects the member version operator new instead of the global version new. But global versions of new and delete are used for all other types of objects (unless they have their own new and delete).

If the operators new and delete are overloaded for a class, these operators will be called whenever an object of that class is created. But if you create an array of these objects, the global operator new() is called immediately to allocate enough memory to the array. The global operator delete() is called to free this memory. You can control the memory allocation of an array of objects by overloading the array version of the operators new [] and delete [] for that class. Here is an example that shows two different versions being called:   In this case, global versions of new and delete are called, and they have the same effect as the unoverloaded versions of new and delete, except that they have added trace information. Of course, we can use the desired memory allocation scheme in the overloaded new and delete.

You can see that the array versions of new and delete are identical to the single-object versions, except for one parenthesis. In both cases, the allocated object memory size is passed. The memory size passed to the array version is the size of the entire array. Remember that the overloaded operator new only needs to return a pointer to a sufficiently large memory. While we can initialize that block of memory, it's usually the job of the constructor, which is automatically called by the compiler.

Thank you for reading, I hope to help you, thank you for your support of this site!


Related articles: